iT邦幫忙

第 11 屆 iThome 鐵人賽

1

成品參考: https://linsheng24.github.io/gatsby-blog/

今天實作留言版的部分,因為react不同於文章,畫面的改變必須馬上反映給使用者,因此不是在build的時候抓取資料的,而是要動態的跟Contentful的API抓取資料。

  1. 首先我們需要在post-template.js中引入一個CommentArea的組件,並放在文章內容下方,此組件需傳入這篇文章的id,所以在graphQL的contentfulPost中許多抓取contentful_id這個field,並傳入postId props,如下:
    src/components/post-template.js
import React from "react"
import Layout from "./layout"
import CommentArea from "./comment-area"

const PostPage = (props) => {
const post = props.data.contentfulPost
...
...
    </article>
    <CommentArea postId={post.contentful_id} />
  </Layout>

  1. 新增src/components/comment-area.js
import React, { useEffect, useState } from "react"
import Comment from "./comment"
import {client, clientManage} from "../cms-config"

const CommentArea = (props) => {
  const postId = props.postId
  const [input, setInput] = useState(
    {
      userId: "",
      text: ""
    }
  )
  const [comments, setComments] = useState([])

  const getComments = () => {
    client.getEntries({
      'sys.id': postId,
    })
    .then((response) => {
      if(response.items[0].fields.hasOwnProperty("comments")) {
        const result = response.items[0].fields.comments.filter(comment => comment.hasOwnProperty("fields"));
        setComments(result)
      }
    })
    .catch(console.error)
  }

  const inputHandler = (e, type) => {
    switch(type){
      case "userId":
        setInput({...input, userId: e.target.value});
        break;
      case "text":
        setInput({...input, text: e.target.value});
        break;
    }
  }

  const clickHandler = () => {
    let newEntry
    const clientSpace = clientManage.getSpace('3uaeu4rxc51m')
    clientSpace
    .then((space) => space.createEntry('comment', {
      fields: {
        userId: {"en-US": input.userId},
        content: {"en-US": input.text}
      }
    }))
    .then((entry) => {
      setInput({userId: "", text: ""})
      entry.publish()
      newEntry = entry
    })
    .then(() => {
      clientSpace
      .then((space)=>space.getEntry(postId))
      .then((entry) => {
        let add = {
        sys:{
          type: "Link",
          linkType: "Entry",
          id: newEntry.sys.id
          }
        }
        if(!entry.fields.hasOwnProperty("comments")){
          entry.fields.comments = {"en-US":[]}
        }
        entry.fields.comments["en-US"].push(add)
        return entry.update()
      })
      .then((entry) => {
        entry.publish()
      })
      .then(()=>{
        setTimeout(getComments,3000)
        
      })
      .catch(console.error)
    })
  }

  useEffect(()=>{
    getComments();
  },[])


  return (
  <>
    <div style={{background: "#f7f7f7", padding: "30px 0"}}>
      <div className="container">
        <div className="row">
          <form className="col-lg-8 col-md-10 mx-auto">
            <div className="form-group">
              <label htmlFor="FormControlInput">使用者名稱</label>
              <input className="form-control" id="FormControlInput" value={input.userId} onChange={(e)=>inputHandler(e, "userId")}/>
            </div>
            <div className="form-group">
              <label htmlFor="FormControlTextarea">留言</label>
              <textarea className="form-control" id="FormControlTextarea" rows="6" value={input.text} onChange={(e)=>inputHandler(e, "text")}></textarea>
            </div>
            <button type="button" className="btn btn-secondary btn-sm" onClick={clickHandler}>Submit</button>
          </form>
        </div>
      </div>

      <div className="container" style={{padding: "70px auto 70px", padding: "30px 20px 0"}}>
        {comments.map((comment)=><Comment key={comment.sys.id} data={comment}/>)}
      </div> 
    </div>
  </>
)}
export default CommentArea

因為程式碼比較常,所以我們分開來看一下,首先是渲染的內容的部分,是使用bootstrap去做簡單做出來的畫面,極度簡化以後,就是只有這樣:

return (
  <>
    <input className="form-control" id="FormControlInput" value={input.userId} onChange={(e)=>inputHandler(e, "userId")}/>
    <textarea className="form-control" id="FormControlTextarea" rows="6" value={input.text} onChange={(e)=>inputHandler(e, "text")}></textarea>
    <button type="button" className="btn btn-secondary btn-sm" onClick={clickHandler}>Submit</button>
    {comments.map((comment)=><Comment key={comment.sys.id} data={comment}/>)}
  </>
)}
  • 裡面有一個input跟textarea,兩個的onChange分別綁定inputHandler(e, "userId")inputHandler(e, "text"),其作用是在輸入文字時,去根據輸入值修改state,詳細可見官方文件:表單
  • button的onClick綁定clickHandler函式,其作用是將輸入框的內容的資料傳到contentful儲存並發布。
  • 最後根據comments,也就是我們用useState做出來的狀態來渲染出多則留言框元件。

  1. 功能部分,之後再回過頭看。 在此之前,樣式有了以後,我們應該想想我們要怎麼在contentful的文章中去儲存留言,我的作法是在Post的Model中,去新增一個Comments的field,他的資料型別是reference,可以在這個型別中存入多筆其他的content資料,簡單講,就是類似陣列的概念。
    https://ithelp.ithome.com.tw/upload/images/20191102/20111629H9SALWMgEp.png
    另外,新增Comment這個Model來作為儲存留言的資料型態
    https://ithelp.ithome.com.tw/upload/images/20191102/20111629W85vpbdOG6.png
    userId是留言者的id,content是留言內容,reply是部落格的版主留言的內容

  1. 有了資料以後,我們下一步就是要試著從comment-area.js中去跟contentful API抓取我的這篇文章的Comments資料
    而抓取文章資料的方法是使用npm install --save contentful安裝contentful的sdk,並引入
var contentful = require('contentful')

如此就能使用了。

而新增或更新資料也是,但是是安裝contentful-management並使用require('contentful-management')

這邊,我新增一個./src/cms-config.js,來存放相關設定:

const contentful = require('contentful')
const contentManage = require('contentful-management')

export const client = contentful.createClient({
  space: '3uaeu4rxc51m',
  accessToken: 'klyMztikgtixidQzIqUmLOdySOM2MPvzmXN_gM17LT8'
})

export const clientManage = contentManage.createClient({
  accessToken: 'CFPAT-nVjBYI3YBDGA9AWFg_SxJhR2uZ-mRTX5pp9RLfB0XZ8'
})

此檔案會匯出兩個物件,提供了我們使用API的方法。
使用方式可參考:
Content Delivery API(抓取內容): https://www.contentful.com/developers/docs/references/content-delivery-api/

Content Management API(傳送內容,發布內容): https://www.contentful.com/developers/docs/references/content-management-api/


  1. 回到comment-area.js的程式碼,分段來看:
import React, { useEffect, useState } from "react"
import Comment from "./comment"
import {client, clientManage} from "../cms-config"

const CommentArea = (props) => {
  const postId = props.postId ///文章id
  const [input, setInput] = useState(
    {
      userId: "",
      text: ""
    }
  )
  const [comments, setComments] = useState([])

  const getComments = () => {
    client.getEntries({
      'sys.id': postId,
    })
    .then((response) => {
      if(response.items[0].fields.hasOwnProperty("comments")) {
        const result = response.items[0].fields.comments.filter(comment => comment.hasOwnProperty("fields"));  //正常情況下應該不需要這個filter(是為了避免在Contentful內的不當操作)
        setComments(result)
      }
    })
    .catch(console.error)
  }
  • 首先,引入client跟clientManage,來實現抓取留言跟送出留言的功能。
  • 使用useState做出兩個state: input和comments,其中,input內為留言的兩個輸入框的值所構成的物件,comments則為該文章的reference所抓到的Comment內容。
  • getComment則是獲取comments的function,先以文章的id獲取文章資料的物件,再去抓取它的comments的內容,hasOwnProperty("comments")是因為在reference為空時,抓到的資料不會預設為空陣列,因此會抓不到屬性。
  • 最後用setComments設定comment狀態,並渲染畫面。

  const inputHandler = (e, type) => {
    switch(type){
      case "userId":
        setInput({...input, userId: e.target.value});
        break;
      case "text":
        setInput({...input, text: e.target.value});
        break;
    }
  }

inputHandler的作用為更新input狀態。


  const clickHandler = () => {
    let newEntry
    const clientSpace = clientManage.getSpace('3uaeu4rxc51m') //使用Manage API
    clientSpace
    .then((space) => space.createEntry('comment', { //新增Comment entry
      fields: {
        userId: {"en-US": input.userId},
        content: {"en-US": input.text}
      }
    }))
    .then((entry) => {
      setInput({userId: "", text: ""}) //新增完後,清空輸入框
      entry.publish()  //發布entry
      newEntry = entry  //將entry存起來,等等會用到
    })
    .then(() => {
      clientSpace
      .then((space)=>space.getEntry(postId)) //取得這篇文章的entry object
      .then((entry) => {
        let add = { //定義新增的資料內容
        sys:{
          type: "Link",
          linkType: "Entry",
          id: newEntry.sys.id // 新增的comment的id
          }
        }
        if(!entry.fields.hasOwnProperty("comments")){
          entry.fields.comments = {"en-US":[]}  //若文章原先沒有comment,則會沒有comment這個屬性,因此須自己新增
        }
        entry.fields.comments["en-US"].push(add)
        return entry.update() //更新資料
      })
      .then((entry) => {
        entry.publish() //發布資料
      })
      .then(()=>{
        setTimeout(getComments,3000) //資料發布完成後,再抓取新的comments陣列來渲染,使用setTimeout做延遲的原因是因為發現若是不延遲會有剛更新完的資料抓不到的情形
        
      })
      .catch(console.error)
    })
  }

按下按鈕->創建新的comment->publish->更新post的comments field->update->publish->重新getComments


  useEffect(()=>{
    getComments();
  },[])

在文章頁面剛載入時,會執行getComments()來抓到文章資料。
useEffect hook的詳細用法可參考官方文件


  1. 最後,當然需要一個Comment組件來顯示文章資料囉,新增src/components/comment.js
import React from "react"

const Comment = (props) => {
  const data = props.data
  return (
    <div className="row">
        <div className="col-lg-9 col-md-10 mx-auto" style={{background: "#F1F1F1", padding: "10px 15px", marginBottom: "20px"}}>
            <h5>{data.fields.userId}</h5>
            <p>{data.fields.content}</p>
            {data.fields.hasOwnProperty("reply")=="" ? "" : <p style={{fontSize: "18px", color: "#888888", background: "#E3E3E3", padding: "20px", margin: "0", maxWidth: "550px"}}>版主: {data.fields.reply}</p>}
            <span style={{float: "right", fontSize: "18px", color: "#888888"}}>Hong edited on {data.sys.createdAt.split('T').shift().split('-').join('/')}</span>
        </div>
    </div>
)}
export default Comment

  1. 完成!!

https://ithelp.ithome.com.tw/upload/images/20191102/20111629Q4b5NBsnmn.png


這次的步驟比較瑣碎也比較多細節,有些瑣碎的修改也不容易講得清楚,要自己去用用看。
所以文章看完有疑問的,可以把github的程式碼抓回去研究,有疑問的也可以留言一起討論。

部落格功能,也算完成得差不多了,雖然還有不少完善的空間(navbar的首頁按鈕,送出留言時的等待畫面),但可能都會留做事後再修改了。

github


上一篇
Day26. 部落格實作 (五)
下一篇
Day28. 結束心得
系列文
用Gatsby.js做出一個簡單的部落格28
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言